package jxtend.processor.eclipse.visitor.handler.impl;

import java.util.Arrays;

import jxtend.processor.eclipse.utils.ASTNodeUtils;
import jxtend.processor.eclipse.utils.EclipseProcessorUtils;

import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.ArrayQualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ArrayTypeReference;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.TypeDeclaration;
import org.eclipse.jdt.internal.compiler.ast.TypeParameter;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.ast.Wildcard;
import org.eclipse.jdt.internal.compiler.impl.ReferenceContext;
import org.eclipse.jdt.internal.compiler.lookup.ArrayBinding;
import org.eclipse.jdt.internal.compiler.lookup.BaseTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.Binding;
import org.eclipse.jdt.internal.compiler.lookup.LookupEnvironment;
import org.eclipse.jdt.internal.compiler.lookup.ParameterizedTypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.ReferenceBinding;
import org.eclipse.jdt.internal.compiler.lookup.Scope;
import org.eclipse.jdt.internal.compiler.lookup.SignatureWrapper;
import org.eclipse.jdt.internal.compiler.lookup.TypeBinding;
import org.eclipse.jdt.internal.compiler.lookup.TypeVariableBinding;

@SuppressWarnings("restriction")
public final class TypeUtils {

	public static boolean isValOrVarType(TypeReference typeReference) {
		if (!(typeReference instanceof SingleTypeReference)) {
			return false;
		}

		SingleTypeReference singleTypeReference = (SingleTypeReference) typeReference;

		boolean isValType = Arrays.equals(singleTypeReference.token, JxtendTypeConstants.VAL);
		boolean isVarType = Arrays.equals(singleTypeReference.token, JxtendTypeConstants.VAR);

		return isValType || isVarType;
	}

	public static TypeReference createTypeReference(TypeBinding typeBinding, long pos) {
		assert typeBinding != null;

		if (typeBinding instanceof BaseTypeBinding) {
			BaseTypeBinding baseTypeBinding = (BaseTypeBinding) typeBinding;
			return new SingleTypeReference(baseTypeBinding.simpleName, pos);
		} else if (typeBinding instanceof ArrayBinding) {
			ArrayBinding arrayBinding = (ArrayBinding) typeBinding;

			if (arrayBinding.leafComponentType instanceof BaseTypeBinding) {
				BaseTypeBinding baseTypeBinding = (BaseTypeBinding) arrayBinding.leafComponentType;
				return new ArrayTypeReference(baseTypeBinding.simpleName, arrayBinding.dimensions, pos);
			} else {
				if (!EclipseProcessorUtils.isValidTypeReference(arrayBinding.leafComponentType)
						|| (arrayBinding.leafComponentType instanceof ParameterizedTypeBinding)) {
					throw new IllegalArgumentException("Can't create type reference for array binding with " +
							" leafComponentType: " + arrayBinding.leafComponentType);
				}

				char[][] compoundName = ((ReferenceBinding) arrayBinding.leafComponentType).compoundName;
				long[] poss = ASTNodeUtils.encodePosArray(compoundName.length, pos);

				return new ArrayQualifiedTypeReference(compoundName, arrayBinding.dimensions, poss);
			}
		} else {
			assert typeBinding instanceof ReferenceBinding;

			if (!EclipseProcessorUtils.isValidTypeReference(typeBinding)) {
				throw new IllegalArgumentException("Can't create type reference for type binding: " + typeBinding);
			}

			char[][] compoundName = ((ReferenceBinding) typeBinding).compoundName;
			long[] poss = ASTNodeUtils.encodePosArray(compoundName.length, pos);

			return new QualifiedTypeReference(compoundName, poss);
		}
	}

	public static TypeBinding createUnresolvedTypeBinding(
			Scope scope, TypeReference typeReference, TypeParameter[] typeParameters) {
		TypeVariableBinding[] typeVariables = Binding.NO_TYPE_VARIABLES;

		if (typeParameters != null && typeParameters.length > 0) {
			int length = typeParameters.length;
			int count = 0;
			typeVariables = new TypeVariableBinding[length];
			for (int j = 0; j < length; j++) {
				if (typeParameters[j].binding != null) {
					typeVariables[count++] = typeParameters[j].binding;
				}
			}
		}

		return createUnresolvedTypeBinding(scope, typeReference, typeVariables);
	}
	
	public static TypeBinding createUnresolvedTypeBinding(
			Scope scope, TypeReference typeReference, TypeVariableBinding[] typeVariables) {
		char[] typeName;
		if (typeReference instanceof SingleTypeReference) {
			typeName = ((SingleTypeReference) typeReference).token;
		} else {
			char[][] tokens = ((QualifiedTypeReference) typeReference).tokens;

			assert tokens.length > 0;

			StringBuilder stringBuilder = new StringBuilder().append(tokens[0]);
			for (int i = 1; i < tokens.length; i++) {
				stringBuilder.append('.').append(tokens[i]);
			}

			typeName = stringBuilder.toString().toCharArray();
		}

		char[] typeSignature = Signature.createCharArrayTypeSignature(typeName, false);
		if (typeSignature[0] == Signature.C_UNRESOLVED) {
			typeSignature[0] = typeReference instanceof Wildcard ? Signature.C_TYPE_VARIABLE : Signature.C_RESOLVED;
		}
		LookupEnvironment lookupEnvironment = scope.referenceCompilationUnit().scope.environment;

		char[][][] missingTypeNames = new char[0][][];
		ReferenceBinding referenceContextBinding = referenceContextBinding(scope.referenceContext());

		return lookupEnvironment.getTypeFromTypeSignature(
				new SignatureWrapper(typeSignature), referenceContextBinding.typeVariables(), referenceContextBinding,
				missingTypeNames);
	}

	public static ReferenceBinding referenceContextBinding(ReferenceContext referenceContext) {
		if (referenceContext instanceof AbstractMethodDeclaration) {
			return ((AbstractMethodDeclaration) referenceContext).binding.declaringClass;
		} else if (referenceContext instanceof TypeDeclaration) {
			return ((TypeDeclaration) referenceContext).binding;
		}

		throw new IllegalArgumentException("Couldn't determine reference binding for reference context type: " +
				referenceContext);
	}
	
	private TypeUtils() {
		// Utility class
	}
}
